/**
 * Created with IntelliJ IDEA.
 * User: seth.yang
 * Date: 15-1-9
 * Time: 下午8:42
 * To change this template use File | Settings | File Templates.
 */
// package define
if (!window.ScienceLib) (function (window) {
    var DefaultLength = 80, PI2 = Math.PI * 2, K = 0.44, G = DefaultLength * DefaultLength,
        DefaultWidth = 800, DefaultHeight = 600;

    // patch requestAnimationFrame
    (function() {
        var lastTime = 0;
        var vendors = ['webkit', 'moz'];
        for(var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) {
            window.requestAnimationFrame = window[vendors[x] + 'RequestAnimationFrame'];
            window.cancelAnimationFrame = window[vendors[x] + 'CancelAnimationFrame'] ||    // Webkit中此取消方法的名字变了
                                          window[vendors[x] + 'CancelRequestAnimationFrame'];

            window.cancelAnimationFrame = window[vendors[x] + 'CancelAnimationFrame'] ||
                                          window[vendors[x] + 'CancelAnimationFrame'] ||
                                          window[vendors[x] + 'CancelRequestAnimationFrame'];
        }

        if (!window.requestAnimationFrame) {
            window.requestAnimationFrame = function(callback) {
                var currTime = new Date().getTime();
                var timeToCall = Math.max(0, 16.7 - (currTime - lastTime));
                var id = window.setTimeout(function() {
                    callback(currTime + timeToCall);
                }, timeToCall);
                lastTime = currTime + timeToCall;
                return id;
            };
        }
        if (!window.cancelAnimationFrame) {
            window.cancelAnimationFrame = function(id) {
                clearTimeout(id);
            };
        }
    }());

    // private method methods.
    var size = function (e) {
            var w = parseInt (e.style.width, 10), h = parseInt (e.style.height, 10);
            w = w || e.clientWidth || e.offsetWidth;
            h = h || e.clientHeight || e.offsetHeight;
            return {width:w,height:h};
        }, createCanvas = function (options) {
            var container, box;
            if ('canvas' in options) {
                container = options ['canvas'];
                if (typeof container === 'string') {
                    container = document.getElementById (container);
                }
                box = size (container);
            } else {
                container = document.createElement ('div');
                container.style.width = DefaultWidth + 'px';
                container.style.height = DefaultHeight + 'px';
                box = {width:DefaultWidth, height:DefaultHeight};
            }
            var canvas = document.createElement ('canvas');
            canvas.width = box.width - 5;
            canvas.height = box.height - 5;
            container.appendChild (canvas);
            return {container:container, canvas:canvas};
        }, extend = function (src, target) {
            var a = {}, k, v;
            if (src) for (k in src) {
                if (src.hasOwnProperty (k)) {
                    v = src [k];
                    if (v) {
                        a [k] = v;
                    }
                }
            }
            if (target) for (k in target) {
                if (target.hasOwnProperty(k)) {
                    v = target [k];
                    if (v) {
                        a [k] = v;
                    }
                }
            }
            return a;
        }, distance = function (a, b) {
        var dx = b.x - a.x, dy = b.y - a.y;
        return Math.sqrt (dx * dx + dy * dy);
    }, tension = function (a, b) {
        var d = distance (a, b);
        if (d <= DefaultLength) {
            return Force.Zero;
        }
        var value = K * (d - DefaultLength), angle = Math.atan2 (b.y - a.y, b.x - a.x);
        return new Force (value, angle);
    }, repulsion = function (a, b) {
        var d = distance (a, b);
        if (d <= 0) return Force.Default;
        var value = G * a.m * b.m / (d * d), angle = Math.atan2 (a.y - b.y, a.x - b.x);
        return new Force (value, angle);
    };

    // -----------------------
    // classe: Force
    // -----------------------
    var Force = function (value, vector) {
            this.angle = vector;
            this.value = value;
            this.x = Math.cos (vector) * value;
            this.y = Math.sin (vector) * value;
        };
    Force.prototype = {
        add : function (force) {
            var dx = this.x + force.x, dy = this.y + force.y;
            this.value = Math.sqrt (dx * dx + dy * dy);
            this.angle = Math.atan2 (dy, dx);
            this.x = this.value * Math.cos (this.angle);
            this.y = this.value * Math.sin (this.angle);
        }
    };
    Force.Default = new Force (10, Math.random() * Math.PI * 2);
    Force.Zero = new Force (0, 0);

    // -----------------------
    // classe: Planet
    // -----------------------
    var Planet = function (m, r, t, dip, name) {
        this.m = m;
        this.r = r;
        this.t = t;
        this.dip = dip;
        this.name = name;
    };

    // -----------------------
    // classe: Coordinates
    // -----------------------
    var C = (function () {
        // private fields
        var ARROW_HEIGHT = 20, ARROW_WIDTH = 4, internal_scale = 50, configs = {};

        // private methods
        var draw = function (x, y, scale, config) {
                var minX = config.minX, minY = config.minY,
                    maxX = config.maxX, maxY = config.maxY,
                    g = config.g;
                x = x * scale * internal_scale;
                y = -y * scale * internal_scale;
                if (x < minX || x > maxX) {
                    g.stroke();
                    g.beginPath();
                    return;
                }
                if (y < minY || y > maxY) {
                    g.stroke();
                    g.beginPath();
                    return;
                }
                g.lineTo (x, y);
            },
            drawArrow = function (g, theta, x, y) {
                var x0, y0, x1, y1, x2, y2;
                if (theta == 0) {
                    x1 = x2 = x - ARROW_HEIGHT;
                    y1 = -ARROW_WIDTH;
                    y2 = ARROW_WIDTH;
                } else if (theta == PI2 / 4) {
                    x1 = -ARROW_WIDTH;
                    x2 = ARROW_WIDTH;
                    y1 = y2 = y + ARROW_HEIGHT;
                } else if (theta == PI2 / 2) {
                    x1 = x2 = x + ARROW_HEIGHT;
                    y1 = -ARROW_WIDTH;
                    y2 = ARROW_WIDTH;
                } else if (theta == 3 * PI2 / 4) {
                    x1 = -ARROW_WIDTH;
                    x2 = ARROW_WIDTH;
                    y1 = y2 = y - ARROW_HEIGHT;
                } else {
                    x0 = x - ARROW_HEIGHT * Math.cos (theta);
                    y0 = y - ARROW_HEIGHT * Math.sin (theta);
                    x1 = x0 + ARROW_WIDTH * Math.sin (theta);
                    y1 = y0 + ARROW_WIDTH * Math.cos (theta);
                    x2 = x0 + ARROW_WIDTH * Math.cos (PI2 / 4 - theta);
                    y2 = y0 + ARROW_WIDTH * Math.sin (PI2 / 4 - theta);
                }
                g.moveTo (x, y);
                g.beginPath();
                g.lineTo (x1, y1);
                g.lineTo (x2, y2);
                g.lineTo (x, y);
                g.closePath ();
                g.fill ();
            }, initPolar = function (steps, step, config) {
                var alpha, delta = Math.PI / 6, x, y, r, g = config.g,
                    max = Math.max (config.maxX, config.maxY);

                r = step;
                for (i = 0; i < steps; i ++) {
                    r = (i + 1) * step;
                    g.beginPath ();
                    g.arc (0, 0, r, 0, PI2, true);
                    g.stroke ();
                }

                for (alpha = Math.PI / 6; alpha < PI2; alpha += delta) {
                    g.beginPath ();
                    x = max * Math.cos (alpha);
                    y = max * Math.sin (alpha);
                    g.moveTo (0, 0);
                    g.lineTo (x, y);
                    g.stroke ();
                }

                g.beginPath ();
                g.strokeStyle = '#999';
                g.lineWidth = 1;
                g['setLineDash'] ([0, 0]);
                g.moveTo (0, 0);
                g.lineTo (max, 0);
                g.stroke ();

                g.font = '6pt Calibri';
                for (var i = 0; i <= steps; i ++) {
                    var n = Math.floor ((i / config.scale) * 100) / 100;
                    g.fillText ('' + n, step * i, 12);
                }

                drawArrow (g, 0, config.maxX, 0);
            }, initCartesian = function (steps, step, config) {
                var i = -steps, x = -step * steps, y = -step * steps, n, g = config.g,
                    maxX = config.maxX, maxY = config.maxY, minX = config.minX, minY = config.minY;
                g.font = '6px Calibri';
                for (; x < maxX; x += step) {
                    g.beginPath ();
                    g.moveTo (x, minY);
                    g.lineTo (x, maxY);
                    g.stroke ();
                    n = Math.floor (((i ++) / config.scale) * 100) / 100;
                    g.fillText ('' + n, x + 2, 15);
                }
                i = -steps;
                for (; y < maxY; y += step) {
                    g.beginPath ();
                    g.moveTo (minX, y);
                    g.lineTo (maxX, y);
                    g.stroke ();

                    if (i != 0) {
                        n = Math.floor ((i / config.scale) * 100) / 100;
                        g.fillText ('' + (-n), 5, y);
                    }
                    i ++;
                }

                g.strokeStyle = '#999';
                g.lineWidth = 1;
                g['setLineDash'] ([0, 0]);
                g.beginPath ();
                g.moveTo (minX, 0);
                g.lineTo (maxX, 0);
                g.moveTo (0, minY);
                g.lineTo (0, maxY);
                g.stroke ();

                drawArrow (g, 0, maxX, 0);
                drawArrow (g, PI2 / 4, 0, minY);
                drawArrow (g, PI2 / 6, 300, 300);
            }, polar2Cart = function (r, theata) {
                var x = r * Math.cos (theata), y = r * Math.sin (theata);
                return {x:x, y:y};
            }, curveCart = function (f, options, config) {
                var scale = config.scale, minX = config.minX, maxX = config.maxX, g = config.g,
                    step = 1 / scale / internal_scale,
                    start = minX / scale / internal_scale,
                    end = maxX / scale / internal_scale,
                    x = start, y = f (x);
                if (options) {
                    if ('start' in options) {
                        start = parseFloat (options ['start']);
                    }
                    if ('end' in options) {
                        end = parseFloat (options ['end']);
                    }
                    if ('step' in options) {
                        step = parseFloat (options ['step']);
                    }
                }
                g.moveTo (x, y);
                g.beginPath ();
                for (x = start; x < end; x += step) {
                    y = f (x);
                    draw (x, y, scale, config);
                }
                g.stroke ();
            }, curvePolar = function (f, options, config) {
                var start = 0, end = 8 * PI2, s = config.scale,
                    g = config.g,
                    step = Math.PI / 180, p = polar2Cart (f (start), start);
                if (options) {
                    if ('start' in options) {
                        start = parseFloat (options ['start']);
                    }
                    if ('end' in options) {
                        end = parseFloat (options ['end']);
                    }
                    if ('step' in options) {
                        step = parseFloat (options ['step']);
                    }
                    if ('scale' in options) {
                        s = parseFloat (options ['scale']);
                    }
                }
                g.moveTo (p.x, p.y);
                g.beginPath ();
                for (var x = start; x < end; x += step) {
                    p = polar2Cart (f (x), x);
                    draw (p.x, p.y, s, config);
                }
                g.stroke ();
                g.closePath ();
            },
            Coordinates = function (options) {
                this.id = Math.random ().toString ().substring (2);
                var tmp = createCanvas (options),
                    canvas = tmp.canvas,
                    g = canvas.getContext ('2d');
                var config = configs [this.id] = {
                    gridStyle : {color: '#dfdfdf', size :.5},
                    lineStyle : {color: '#99f', size: 1},
                    type : 'Cartesian',
                    canvas : tmp.canvas,
                    g : g,
                    center : {x:canvas.width / 2, y:canvas.height / 2},
                    scale : ('scale' in options) ? parseFloat (options ['scale']) : 1.0,
                    options : options
                };
                g.translate(config.center.x, config.center.y);
            };
        Coordinates.prototype = {
            init : function () {
                var config = configs [this.id],
                    options = config.options,
                    g = config.g,
                    canvas = config.canvas,
                    gridStyle = config.gridStyle,
                    lineStyle = g.lineStyle,
                    max = Math.floor (Math.max (canvas.width, canvas.height) / 2);
                if ('scale' in options) config.scale = parseFloat (options ['scale']);
                var steps = parseInt(max / internal_scale);

                if ('gridStyle' in options) {
                    config.gridStyle = extend (config.gridStyle, options ['gridStyle']);
                }
                if ('lineStyle' in options) {
                    config.lineStyle = extend (config.lineStyle, options ['lineStyle']);
                }
                if ('type' in options) {
                    config.type = options ['type'];
                }

                config.minX = -(canvas.width / 2);
                config.maxX = canvas.width / 2;
                config.minY = -(canvas.height / 2);
                config.maxY = canvas.height / 2;

                g.fillStyle = '#fff';
                g.fillRect (config.minX, config.minY, canvas.width, canvas.height);
                g.fillStyle = '#999';
                g.font = '20px Calibri';
                g.fillText ('scale: ' + config.scale, config.minX, config.minY + 20);

                g.strokeStyle = gridStyle.color;
                g['setLineDash'] ([3, 3]);
                g.lineWidth = gridStyle.size;
                switch (config.type) {
                    case 'Cartesian' :
                        initCartesian (steps, internal_scale, config);
                        break;
                    case 'Polar' :
                        initPolar (steps, internal_scale, config);
                        break;
                }

                return this;
            },
            curve : function (f, options) {
                var config = configs [this.id], g = config.g;
                if (options) {
                    if (('lineStyle' in options)) {
                        config.lineStyle = extend (config.lineStyle, options ['lineStyle']);
                    }
                }

                g.lineWidth = config.lineStyle.size;
                g.strokeStyle = config.lineStyle.color;

                if (config.type == 'Cartesian') {
                    curveCart (f,options, config);
                } else if (config.type == 'Polar') {
                    curvePolar (f, options, config);
                }
                return this;
            },
            clear : function () {
                this.init ();
                return this;
            }
        };

        return Coordinates;
    }) ();

    // -----------------------
    // classe: Layout
    // -----------------------
    var L = (function () {
        var configs = {};
        // configurable infos
        var textColor = '#8888ff', nodeColor = '#ffa500', lineColor = '#cccccc',
            padding = 5;
        var draw = function (config) {
            var node, link, i, g = config.g, canvas = config.canvas,
                links = config.links, nodes = config.nodes;
            g.clearRect (0, 0, canvas.width, canvas.height);

            g.strokeStyle = config.lineColor || lineColor;
            for (i = 0; i < links.length; i ++) {
                link = links [i];
                g.beginPath ();
                g.moveTo (link.a.x, link.a.y);
                g.lineTo (link['z'].x, link['z'].y);
                g.stroke ();
            }

            for (i = 0; i < nodes.length; i ++) {
                node = nodes [i];
                var r0 = .5, r1 = 7 * node.r / 4, x = node.x - 2, y = node.y - 2,
                    gradient = g.createRadialGradient (x, y, r0, x, y, r1);
                gradient.addColorStop (0, '#ffffff');
                gradient.addColorStop (1, config.nodeColor || nodeColor);
                g.fillStyle = gradient;
                g.beginPath ();
                g.arc (node.x, node.y, node.r, 0, PI2, false);
                g.closePath ();
                g.fill ();
                if (config['showText']) {
                    g.fillStyle = config.textColor || textColor;
                    g.fillText (node.label, node.x, node.y);
                }
            }
        },
        contains = function (container, node, x, y) {
            x += container.scrollLeft;
            y += container.scrollTop;
            var dx = node.x - x, dy = node.y - y;
            var d = Math.sqrt (dx * dx + dy * dy);
            return d <= node.r;
        },
        move = function (node) {
            if (node.locked) return;
            var d = node.f.value / node.m;
            node.x += d * Math.cos (node.f.angle);
            node.y += d * Math.sin (node.f.angle);

            node.f.angle = 0;
            node.f.value = 0;
            node.f.x = 0;
            node.f.y = 0;
        },
        adjustCanvas = function (nodes, padding, canvas) {
            var max = {width:-1,height:-1}, node, d, i, j;
            for (i = 0; i < nodes.length; i ++) {
                node = nodes [i];
                if (node.x - node.r < padding) {
                    d = padding - node.x + node.r;
                    for (j = i; j < nodes.length; j ++) {
                        nodes [j].x += d;
                    }
                }
                if (node.y - node.r < padding) {
                    d = padding - node.y + node.r;
                    for (j = i; j < nodes.length; j ++) {
                        nodes [j].y += d;
                    }
                }

                if (node.x + node.r + padding > max.width) {
                    max.width = node.x + node.r + padding;
                }
                if (node.y + node.r + padding > max.width) {
                    max.height = node.y + node.r + padding;
                }
            }
            if (max.width > canvas.width)
                canvas.width = max.width;
            if (max.height > canvas.height)
                canvas.height = max.height;
        },
        doLayout = function (c) {
            if (c['jitter']) {
                clearInterval (c.handler);
                return;
            }

            var a, i, j, z, links = c.links, nodes = c.nodes;
            for (i = 0; i < links.length; i ++) {
                a = links [i].a;
                z = links [i]['z'];
                a.f.add (tension (a, z));
                z.f.add (tension (z, a));
            }

            for (i = 0; i < nodes.length; i ++) {
                var n1 = nodes [i];
                for (j = 0; j < nodes.length; j ++) {
                    var n2 = nodes [j];
                    if (i != j) {
                        n1.f.add (repulsion (n1, n2));
                    }
                }
            }

            for (i = 0; i < nodes.length;i ++) {
                move (nodes [i]);
            }

            adjustCanvas (nodes, padding, c.canvas);

            c.jitter = true;
            for (i = 0; i < nodes.length; i ++) {
                if (!nodes [i].locked) {
                    c.jitter = false;
                    break;
                }
            }

            draw (c);

            c ['handler'] = requestAnimationFrame (function () {
                doLayout (c);
            });
        },
        attachEvents = function (c) {
            c.canvas.onmousedown = function (e) {
                var x = e.x || e.clientX, y = e.y || e.clientY, found = false,
                    nodes = c.nodes, delta = c.delta;
                if (!delta) {
                    delta = c.delta = {x:0,y:0};
                }
                for (var i = 0; i < nodes.length; i ++) {
                    if (contains (c.container, nodes [i], x, y)) {
                        c.pickedUp = nodes [i];
                        delta.x = nodes [i].x - x;
                        delta.y = nodes [i].y - y;
                        break;
                    }
                }

                if (!found) {
                    c.canvas.style.cursor = 'default';
                }
            };
            c.canvas.onmousemove = function (e) {
                var pickedUp = c.pickedUp, nodes = c.nodes, delta = c.delta;
                if (!delta) {
                    delta = c.delta = {x:0,y:0};
                }
                if (pickedUp) {
                    pickedUp.x = e.x || e.clientX + delta.x;
                    pickedUp.y = e.y || e.clientY+ delta.y;
                    draw (c);
                } else {
                    var found = false;
                    for (var i = 0; i < nodes.length; i ++) {
                        if (contains (c.container, nodes[i], e.x|| e.clientX, e.y|| e.clientY)) {
                            found = true;
                            break;
                        }
                    }

                    if (found)
                        c.canvas.style.cursor = 'pointer';
                    else
                        c.canvas.style.cursor = 'default';
                }
            };
            c.canvas.onmouseup = function () {
                delete c.pickedUp;
            };
            c.canvas.mouseout = function () {
                delete c.pickedUp;
            };
        };

        var Layout = function (options) {
            var id = 'Layout_' + Math.random().toString ().substring (2);
            var config = configs [id] = {};
            this.id = id;
            this.applyConfig (options);

            this.init ();
            attachEvents (config);
        };

        Layout.prototype = {
            applyConfig : function (options) {
                if (!('nodes' in options)) throw 'no nodes found!';
                if (!('links' in options)) throw 'no links found!';
                var config = configs [this.id];
                config.nodeColor = options ['nodeColor'] || nodeColor;
                config.lineColor = options ['lineColor'] || lineColor;
                config.textColor = options ['textColor'] || textColor;
                config.padding   = options ['padding']   || padding;

                config.nodes = options ['nodes'];
                config.links = options ['links'];
                config.interval = ('interval' in options) ? options ['interval'] : 100;

                if ('canvas' in options) {
                    config.options = {'canvas': options ['canvas']};
                }

				config.showText = options ['showText'];
            },
            start : function () {
                var config = configs [this.id];
                if (!(('handler' in config) && config.running)) {
                    doLayout (config);
                    config.running = true;
                }
            },
            stop : function () {
                var config = configs [this.id];
                if ('handler' in config && config.handler) {
                    config.running = false;
                    delete config.running;
                    window.cancelAnimationFrame (config.handler);
                }
            },
            init : function () {
                var config = configs [this.id],
                    links = config.links,
                    nodes = config.nodes,
                    tmp = createCanvas (config.options || {}),
                    canvas = config.canvas = tmp.canvas;
                config.g = canvas.getContext('2d');
                config.container = tmp.container;
                for (var i = 0; i < links.length; i ++) {
                    if ('m' in links [i].a)
                        links [i].a.m ++;
                    else
                        links [i].a.m = 1;

                    if ('m' in links [i].z)
                        links [i].z.m ++;
                    else
                        links [i].z.m = 1;
                }

                for (i = 0; i < nodes.length; i ++) {
                    nodes [i].x = Math.random () * canvas.width;
                    nodes [i].y = Math.random () * canvas.height;
                    nodes [i].f = new Force (0, 0);
                }

                for (i = 0; i < nodes.length; i ++) {
                    nodes [i].r = 5 + nodes[i].m;
                }
                adjustCanvas (nodes, padding, canvas);
                draw (config);
                console.debug (configs);
            },
            reset : function () {
                var nodes = configs [this.id].nodes;
                for (var i = 0; i < nodes.length;i ++) {
                    if (nodes [i].m)
                        delete nodes [i].m;
                    if (nodes [i].r)
                        delete nodes [i].r;
                }
            },
            dispose : function () {
                delete configs [this.id];
                console.debug (configs);
            }
        };

        return Layout;
    })();

    // -----------------------
    // classe: Sun
    // -----------------------
    var S = (function () {
        var bg = 'rgba (255,255,255,0.6)',
            configs = {};

        var Solar = function (options) {
            this.id = 'Solar_' + Math.random().toString ().substring (2);
            var config = configs [this.id] = {};
            if (!('planets' in options)) {
                throw 'planets can not be empty!';
            }
            config.planets = options ['planets'];
        };
        Solar.prototype = {
            start : function () {},
            stop  : function () {},
            dispose : function () {}
        };
        return Solar;
    })();

    window.ScienceLib = {
        Geometry :{
            Coordinates : C
        },
        PhysicsModel : {
            Force : Force,
            Layout : L,
            Solar : S
        }
    };
}) (window);